<pre class="metadata">
Title: Contact Picker API
Shortname: contact-picker
Level: 1
Status: UD
Group: wicg
URL: https://wicg.github.io/contact-api/spec
Editor: Peter Beverloo, Google, beverloo@google.com
Editor: Rayan Kanso, Google, rayankans@google.com
Abstract: An API to give one-off access to a user's contact information with full control over the shared data.
Markup Shorthands: css no, markdown yes
Indent: 2
</pre>

<pre class=link-defaults>
spec:infra; type:dfn; text:list
spec:html; type:dfn; for:/; text:browsing context
spec:html; type:dfn; for:/; text:origin
spec:html; type:dfn; for:/; text:valid e-mail address
</pre>

<pre class="anchors">
spec: payment-request; urlPrefix: https://www.w3.org/TR/payment-request/
  type: dfn; text: physical addresses; url: physical-addresses
  type: interface; text: PaymentAddress; url: dom-paymentaddress
</pre>

# Introduction # {#intro}

Contact pickers are frequently seen in various desktop and native mobile applications for a variety
of use cases. This specification defines an API to bring contact pickers to the web, which will
enable new use cases for web apps, such as:
* Bootstrapping a user's social graph for social networks.
* Selecting the recipients of a message within an e-mail application.

The contact picker model was chosen to give full control to users over the shared data, allowing
users to choose exactly which contacts to provide to the website. The contact picker model gives
websites one-off access to a user's contacts, meaning developers have to request access to the
user's contacts every time they need it. This differs from some native contact APIs, but is
necessary for ensuring users' contacts are not accessed without their knowledge and explicit
consent.

## Examples ## {#examples}

<div class="example">
  Requesting contacts as a result of a user click.

  <pre class="lang-js">
    selectRecipientsButton.addEventListener('click', async () => {
      const contacts = await navigator.contacts.select(['name', 'email'], {multiple: true});

      if (!contacts.length) {
        // No contacts were selected in the picker.
        return;
      }

      // Use the names and e-mail addresses in |contacts| to populate the
      // recipients field in the website's UI.
      populateRecipients(contacts);
    });
  </pre>

  In the above example `selectRecipientsButton` is a {{HTMLButtonElement}}, and `populateRecipients`
  is a developer-defined function.
</div>


<div class="example">
  Requesting an address to deliver a gift to.

  <pre class="lang-js">
    selectRecipientButton.addEventListener('click', async () => {

      // We are unsure if addresses are supported, or can be provided by the browser.
      if ((await navigator.contacts.getProperties()).includes('address')) {
        const contacts = await navigator.contacts.select(['address']);

        if (!contacts.length) {
          // No contacts were selected in the picker.
          return;
        }

        // length is 1 since we didn't request multiple contacts.
        sendGiftToAddress(contacts[0].address);
      }

     // Fallback to a form. 
    });
  </pre>

  In the above example `selectRecipientButton` is a {{HTMLButtonElement}}, and `sendGiftToAddress`
  is a developer-defined function.
</div>

<div class="example">
  Requesting a name and an icon.

  <pre class="lang-js">
    selectRecipientButton.addEventListener('click', async () => {

      // We are unsure if icons are supported, or can be provided by the browser.
      if ((await navigator.contacts.getProperties()).includes('icon')) {
        const contacts = await navigator.contacts.select(['name', 'icon']);

        if (!contacts.length) {
          // No contacts were selected in the picker.
          return;
        }

        if (!contacts[0].name.length || !contacts[0].icon.length) {
          // Info not found. Use fallback.
          return;
        }

        // We only need one name and one image.
        const name = contacts[0].name[0];
        const imgBlob = contacts[0].icon[0];

        // Display image.
        const url = URL.createObjectURL(imgBlob);
        imgContainer.onload = () => URL.revokeObjectURL(url);
        imgContainer.src = url;

        // Alternatively use a Bitmap.
        const imgBitmap = await createImageBitmap(imgBlob);

        // Upload icon.
        const response = await fetch('/contacticon', {method: 'POST', body: imgBlob});
      }
    });
  </pre>
  In the above example `selectRecipientButton` is a {{HTMLButtonElement}}, and `imgContainer`
  is a {{HTMLImageElement}}.
</div>

# Privacy Considerations # {#privacy}

Exposing contact information has a clear privacy impact, in terms of exposing PII of uninvolved
parties. A picker model is enforced so that the user agent can offer a user experience that makes
it clear what information is going to be shared with the website and when.

The following constraints are also enforced:
* The API is only available in a [=top-level browsing context=] which must also be a
  [=secure context=]. These restrictions help ensure that the provided contact information reaches
  its intended recipient.
* A user gesture is needed to initiate the API, to disallow programmatic requests to the user's
  contacts.

# Realms # {#realms}

All platform objects are created in the [=context object=]'s [=relevant Realm=] unless otherwise
specified.

# Infrastructure # {#infrastructure}

The <dfn>contact picker task source</dfn> is a [=task source=].

<div algorithm>
  To <dfn>queue a contact picker task</dfn> on an optional |eventLoop| (an [=event loop=],
  defaulting to the caller's [=context object=]'s [=relevant settings object=]'s
  [=responsible event loop=]) with |steps| (steps), [=queue a task=] on |eventLoop| using the
  [=contact picker task source=] to run |steps|.
</div>

## User contact ## {#infrastructure-user-contact}

A <dfn>user contact</dfn> consists of:
<div dfn-for="user contact">

* <dfn>names</dfn>, a [=list=] of {{DOMString}}s, each [=list/item=] representing a unique name
  corresponding to the user.
* <dfn>emails</dfn>, a [=list=] of {{DOMString}}s, each [=list/item=] representing a unique
  [=valid e-mail address=] of the user.
* <dfn>numbers</dfn>, a [=list=] of {{DOMString}}s, each [=list/item=] representing a unique phone
  number of the user.
* <dfn>addresses</dfn>, a [=list=] of {{ContactAddress}}es, each [=list/item=] representing a
  unique [=physical address=] of the user.
* <dfn>icons</dfn>, a [=list=] of {{Blob}}s, each [=list/item=] representing a unique image of the
  user.
    
    NOTE: An icon {{Blob}}'s {{Blob/type}} is an [=image mime type=].

A [=user contact=] contains data relating to a single user.

Note: The lists can be of different sizes, and entries with the same index don't need to correspond
to each other.

## Contacts source ## {#infrastructure-contacts-source}

The <dfn>contacts source</dfn> is a service that provides the user's contact information to
the user agent.

A [=contacts source=] consists of:
<div dfn-for="contacts source">

* <dfn>available contacts</dfn>, a [=list=] of [=user contacts=].
* <dfn>supported properties</dfn>, a [=list=] of [=available=] {{ContactProperty}} values.

</div>

Note: It is up to the user agent to choose the [=contacts source=].

</div>

# API Description # {#api}

## Extensions to {{Navigator}} ## {#extensions-to-navigator}

<script type="idl">
[Exposed=Window]
partial interface Navigator {
  [SecureContext, SameObject] readonly attribute ContactsManager contacts;
};
</script>

<div dfn-for="Navigator">
A {{Navigator}} has a <dfn>contacts manager</dfn> (a {{ContactsManager}}), initially a new
{{ContactsManager}}.

The <dfn attribute>contacts</dfn> attribute's getter must return the [=context object=]'s
[=Navigator/contacts manager=].
</div>

The [=browsing context=] has a <dfn>contact picker is showing flag</dfn>, initially unset.

## {{ContactProperty}} ## {#contact-property}

<script type="idl">
enum ContactProperty { "address", "email", "icon", "name", "tel" };
</script>

A {{ContactProperty}} is considered to be <dfn>available</dfn> if its associated [=user contact=]
field can be accessed by the user agent.

: "address"
:: Associated with [=user contact=]'s [=user contact/addresses=].
: "email"
:: Associated with [=user contact=]'s [=user contact/emails=].
: "icon"
:: Associated with [=user contact=]'s [=user contact/icons=].
: "name"
:: Associated with [=user contact=]'s [=user contact/names=].
: "tel"
:: Associated with [=user contact=]'s [=user contact/numbers=].

## {{ContactsManager}} ## {#contacts-manager}

<script type="idl">
interface ContactAddress : PaymentAddress {};

dictionary ContactInfo {
    sequence<ContactAddress> address;
    sequence<DOMString> email;
    sequence<Blob> icon;
    sequence<DOMString> name;
    sequence<DOMString> tel;
};

dictionary ContactsSelectOptions {
    boolean multiple = false;
};

[Exposed=(Window,SecureContext)]
interface ContactsManager {
    Promise<sequence<ContactProperty>> getProperties();
    Promise<sequence<ContactInfo>> select(sequence<ContactProperty> properties, optional ContactsSelectOptions options);
};
</script>

<div dfn-for="ContactsManager">

### {{ContactsManager/getProperties()}} ### {#contacts-manager-getproperties}

<div algorithm>
  The <dfn method>getProperties()</dfn> method, when invoked, runs these steps:

  1. Let |promise| be [=a new promise=].
  1. Run the following steps [=in parallel=]:
      1. Resolve |promise| with [=contacts source=]'s [=contacts source/supported properties=].
  1. Return |promise|.

</div>

### {{ContactsManager/select()}} ### {#contacts-manager-select}

<div algorithm>
  The <dfn method>select(|properties|, |options|)</dfn> method, when invoked, runs these steps:

  1. Let |relevantBrowsingContext| be the [=context object=]'s [=relevant settings object=]'s
     [=environment settings object/responsible browsing context=].
  1. If |relevantBrowsingContext| is not a [=top-level browsing context=], then return
     [=a promise rejected with=] an {{InvalidStateError}} {{DOMException}}.
  1. If the algorithm is not [=triggered by user activation=] then return
     [=a promise rejected with=] a {{SecurityError}} {{DOMException}}.
  1. If |relevantBrowsingContext|'s [=contact picker is showing flag=] is set then return
     [=a promise rejected with=] an {{InvalidStateError}} {{DOMException}}.
  1. If |properties| is [=list/empty=], then return [=a promise rejected with=] a {{TypeError}}.
  1. [=list/For each=] |property| of |properties|:
    1. If [=contacts source=]'s [=contacts source/supported properties=] does not [=list/contain=]
        |property|, then return [=a promise rejected with=] a {{TypeError}}.
  1. Set |relevantBrowsingContext|'s [=contact picker is showing flag=].
  1. Let |promise| be [=a new promise=].
  1. Run the following steps [=in parallel=]:
    1. Let |selectedContacts| be be the result of [=launching a contact picker=] with |options|'
       `multiple` member and |properties|. If this fails, then:
      1. Return [=a promise rejected with=] an {{InvalidStateError}} {{DOMException}}.
      1. Unset |relevantBrowsingContext|'s [=contact picker is showing flag=].
      1. Abort these steps.
    1. Unset |relevantBrowsingContext|'s [=contact picker is showing flag=].
    1. [=Queue a contact picker task=] to run these steps:
      1. Let |contacts| be an empty [=list=].
      1. [=list/For each=] |selectedContact| in |selectedContacts|:
        1. Let |contact| be a new {{ContactInfo}} with:
          : {{ContactInfo/address}}
          :: |selectedContact|'s [=user contact/addresses=] if |properties| [=list/contains=]
            "`address`", otherwise undefined.
          : {{ContactInfo/email}}
          :: |selectedContact|'s [=user contact/emails=] if |properties| [=list/contains=]
             "`email`", otherwise undefined.
          : {{ContactInfo/icon}}
          :: |selectedContact|'s [=user contact/icons=] if |properties| [=list/contains=]
            "`icon`", otherwise undefined.
          : {{ContactInfo/name}}
          :: |selectedContact|'s [=user contact/names=] if |properties| [=list/contains=]
             "`name`", otherwise undefined.
          : {{ContactInfo/tel}}
          :: |selectedContact|'s [=user contact/numbers=] if |properties| [=list/contains=]
             "`tel`", otherwise undefined.
        1. [=list/Append=] |contact| to |contacts|.
      1. Resolve |promise| with |contacts|.
  1. Return |promise|.
</div>

# Contact Picker # {#contact-picker}

<div algorithm>
  To <dfn lt="launching a contact picker">launch</dfn> a contact picker with |allowMultiple| (a
  [=boolean=]), and |properties| (a [=list=] of {{DOMString}}s), the user agent MUST present a user
  interface that follows these rules:

  * If presenting a user interface fails or accessing the [=contacts source=]'s
    [=contacts source/available contacts=] fails, then return failure.   
  * The UI MUST prominently display the [=browsing context=]'s [=origin=].
  * The UI MUST make it clear which `properties` of the contacts are requested.

    NOTE: This information is derived from |properties|.
  
  * The UI SHOULD provide a way for users to opt out of sharing certain contact information. 

    NOTE: If the user opts out, the appropriate [=user contact=] fields should be modified before
    returning the selected contacts. It should be indistinguishable from the returned
    [=user contact=]s whether the user opted out from sharing some information or if the
    information was not present to begin with.

  * The UI MUST make it clear which information will be shared. 
  * The UI MUST provide a way to select individual contacts. If |allowMultiple| is false, only one
    contact should be pickable.
  * The UI MUST provide an option to cancel/return without sharing any contacts, in which case
    remove the UI and return an empty [=list=].
  * The UI MUST provide an a way for users to indicate that they are done selecting, in which case
    remove the UI and return a [=list=] of the selected contacts as [=user contacts=].
</div>
